﻿using DeepZoomPivotConstructor.ErrorAndLoggingInterfaces;
using DeepZoomPivotConstructor.VisualCacheInterfaces;
using DeepZoomPivotConstructor.VisualLib;
using DeepZoomPivotConstructor.VisualLib.Hotspots;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Serialization;

namespace DeepZoomPivotConstructor.DTBuilderLib
{
    /// <summary>
    /// Manage the interface to the deep zoom composer utilities.
    /// </summary>
    public class DeepZoomBuilder
    {

        /// <summary>
        /// The image quality... 0 to 1.
        /// </summary>
        public double ImageQuality { get; set; }

        /// <summary>
        /// Set defaults.
        /// </summary>
        public DeepZoomBuilder(IErrorLogger errorLogger)
        {
            if (errorLogger == null)
                throw new ArgumentNullException("errorLogger cannot be null!");

            ImageQuality = 1.0;
            SubMeetingDir = @"sc";
            MyVisualDB = null;
            MyErrorLogger = errorLogger;
        }

        /// <summary>
        /// Create a Builder with a visual cache to speed up the rendering if something
        /// has previously been rendered.
        /// </summary>
        /// <param name="errorLogger"></param>
        /// <param name="cacheDB"></param>
        public DeepZoomBuilder(IErrorLogger errorLogger, IVisualCacheDB cacheDB)
            : this(errorLogger)
        {
            MyVisualDB = cacheDB;
        }

        /// <summary>
        /// Hold onto the cache that we are going to use
        /// </summary>
        private IVisualCacheDB MyVisualDB { get; set; }

        /// <summary>
        /// Hold onto the error logger we will use.
        /// </summary>
        private IErrorLogger MyErrorLogger { get; set; }

        /// <summary>
        /// Given the root visual item, render it to XML and then run the composer utilties
        /// on it. Leave behind a token file to 
        /// </summary>
        /// <param name="root">The visual hierarchy to render to a compser scene.</param>
        /// <param name="meetingDir">The directory where the output will be written</param>
        /// <returns>The image number for each image in the list of images.</returns>
        /// <remarks>
        /// The image number may skip - if a an old item is removed from the list of rendered images
        /// (there is a data-base remembering feature for teh main collection image).
        /// </remarks>
        public async Task<IEnumerable<ulong>> RenderVisual(DirectoryInfo meetingDir, Visual root)
        {
            ///
            /// We are outta here if there is a null sitting in here!
            /// 

            if (root == null)
            {
                return null;
            }

            ///
            /// Make sure to add some info onto the exception adn log it on the way out -- but we don't block the
            /// exception with the following try block.
            /// 

            try
            {
                meetingDir.Create();
                meetingDir.Refresh();

                ///
                /// 
                /// Rescal so that the max size is 1.0. The reaons has to do with the sparse image tool's
                /// coordinate system. A size of 1.0 means that the image takes up the full view port.
                /// What is what we want, of course.
                /// 
                /// The system seems to use the width as the measuring stick, and so we only scale
                /// w.r.t. the width rather than making sure the whole thing fits on the display
                /// screen.
                /// 

                double scale = 1.0 / root.Width;
                root.Scale(scale);

                ///
                /// Next task is we have to move through the hierarchy looking for visuals that are compositions. And any
                /// images that aren't gathered into compositions we need to put into 
                /// 

                var allTopLevelImages = from v in root.GetHierarchicalEnumerator(v => v.IsCompositionUnit)
                                        select v;

                ///
                /// Get a list of all the sub-visuals we are going to have to process. This means all the compositions
                /// along with all the left over background images.
                /// 
                /// WARNING: VisualHolder grabs the LOWEST z index of any image and call that it's ZIndex. The background code below will break
                /// if that behavior changes (the background might float up in front of some other composite image).
                /// 

                var topLevelCompositionVisuals = from v in allTopLevelImages
                                                 where v.IsCompositionUnit == true
                                                 select v;
                List<Visual> visualsToRender = new List<Visual>(topLevelCompositionVisuals);

                var allbackgroundImages = from v in allTopLevelImages
                                          where v.IsCompositionUnit == false
                                          select v;

                if (allbackgroundImages.Count() == 1)
                {
                    visualsToRender.Add(allbackgroundImages.First());
                }
                else if (allbackgroundImages.Count() > 1)
                {
                    VisualHolder masterBackgroundHolder = new VisualHolder();
                    foreach (var v in allbackgroundImages)
                    {
                        masterBackgroundHolder.AddImage(v);
                    }
                    visualsToRender.Add(masterBackgroundHolder);
                }

                ///
                /// Next, render everything!
                /// 

                var renderedCompositions = (from v in visualsToRender
                                            where v != null
                                            let imageFInfo = ConvertComposition(v, meetingDir)
                                            where imageFInfo != null
                                            let bb = v.VisualBoundingBox
                                            select new
                                            {
                                                ImageFInfo = imageFInfo,
                                                metaData = new DZTalkVisualMetaData
                                                {
                                                    Width = bb.Width,
                                                    Height = bb.Height,
                                                    X = bb.XPosition,
                                                    Y = bb.YPosition,
                                                    Z = v.ZIndex - 1,
                                                    RelativeName = Path.ChangeExtension(imageFInfo.FullName.Substring(meetingDir.FullName.Length + 1), null)
                                                }
                                            }).ToArray();

                ///
                /// Next write out all the metadata for these images. This is, really, just the position and
                /// Z index of all the images.
                ///

                var imageMetadataList = from v in renderedCompositions select v.metaData;

                using (var text = File.CreateText(meetingDir.FullName + "\\meetingMetadata.xml"))
                {
                    XmlSerializer xs = new XmlSerializer(typeof(DZTalkVisualMetaData[]));
                    xs.Serialize(text, imageMetadataList.ToArray());
                    text.Close();
                }

                ///
                /// Get rid of any sub-images that we aren't using now. This happens most
                /// frequently when we re-render things - we aren't currenlty re-using what is
                /// there - so we should get rid of the old stuff.
                /// 

                var subDirsInUse = from v in renderedCompositions
                                   where v.ImageFInfo != null
                                   select v.ImageFInfo.Name.Substring(0, v.ImageFInfo.Name.Length - 4);

                DirectoryInfo subMeetingDir = new DirectoryInfo(meetingDir + "\\" + SubMeetingDir);
                if (subMeetingDir.Exists)
                {
                    var dirsToDelete = from d in subMeetingDir.GetDirectories()
                                       where !subDirsInUse.Contains(d.Name.Substring(0, d.Name.IndexOf("_files")))
                                       select d;
                    foreach (var d in dirsToDelete)
                    {
                        /// Sometimes a web server, etc., has locked this guy - we don't care, we'll get
                        /// it next time we run an update...
                        try
                        {
                            d.Delete(true);
                        }
                        catch { }
                    }

                    var xmlFilesToDelete = from f in subMeetingDir.GetFiles()
                                           where f.Extension == ".xml" && !subDirsInUse.Contains(GetRenderBaseName(f.Name))
                                           select f;
                    foreach (var f in xmlFilesToDelete)
                    {
                        f.Delete();
                    }
                }

                ///
                /// Now, put everything else together in a final image
                /// 

                var cr = new DZParallelLib.RenderDZCollectionFromImages();
                var outputName = new FileInfo(string.Format(@"{0}\{1}.xml", meetingDir.FullName, meetingDir.Name));
                Func<FileInfo, string> cvt = fi => fi.FullName.Substring(fi.FullName.IndexOf(@"\sc\") + 1).Replace(@"\", "/");
                var tmp = renderedCompositions.Select(i => i.ImageFInfo).ToArray();
                var orderedImages = await cr.RenderCollection(renderedCompositions.Select(i => i.ImageFInfo), cvt, outputName);

                return orderedImages;
            }
            catch (Exception e)
            {
                StringBuilder msg = new StringBuilder();
                msg.Append("Error rendering meeting " + meetingDir.Name + " - ");
                if (root.Info != null && root.Info.Title != null)
                {
                    msg.Append(root.Info.Title);
                }
                else
                {
                    msg.Append("untitled visual");
                }
                MyErrorLogger.LogMessage("Error Rendering", msg.ToString(), e);
                throw;
            }
        }

        /// <summary>
        /// Gets teh base name - removes the hotspot in the filename or the .xml...
        /// </summary>
        /// <param name="p"></param>
        /// <returns></returns>
        private string GetRenderBaseName(string baseFName)
        {
            int dash = baseFName.IndexOf("-");
            if (dash < 0)
            {
                dash = baseFName.Length - 4;
            }
            return baseFName.Substring(0, dash);
        }

        /// <summary>
        /// Given a list of visuals that we are going to be looking at - extract the visuals that
        /// are hot spots, and write their data to the specified file.
        /// </summary>
        /// <param name="hsFile">hot spot xml filename for output</param>
        /// <param name="allTopLevelImages">Iterator over the images</param>
        private static void ExtractHSInfo(string hsFile, IEnumerable<Visual> allTopLevelImages)
        {
            ///
            /// Find all the HS objects
            /// 

            var hotspotVisuals = from v in allTopLevelImages
                                 where v.GetType() == typeof(VisualHotSpot)
                                 let hs = v as VisualHotSpot
                                 select new DZHotSpotMetaData
                                 {
                                     LinkLocation = new DZTalkVisualMetaData()
                                     {
                                         Height = hs.Height,
                                         Width = hs.Width,
                                         X = hs.XPosition,
                                         Y = hs.YPosition,
                                         Z = hs.ZIndex
                                     },
                                     Actions = ConvertToDZActions(hs)
                                 };

            ///
            /// Write them out!
            /// 

            using (var text = File.CreateText(hsFile))
            {
                XmlSerializer xs = new XmlSerializer(typeof(DZHotSpotMetaData[]));
                xs.Serialize(text, hotspotVisuals.ToArray());
                text.Close();
            }
        }

        /// <summary>
        /// Given the list of actions from the Visual world, convert it to actions that
        /// can be stored in our simple i/o format.
        /// </summary>
        /// <param name="hs"></param>
        /// <returns></returns>
        private static List<DZHotSpotMetaData.ActionData> ConvertToDZActions(VisualHotSpot hs)
        {
            Trace.WriteLine("Extracting hotspots");
            List<DZHotSpotMetaData.ActionData> result = new List<DZHotSpotMetaData.ActionData>();
            hs.GetAllActoinData((ainfo) =>
                {
                    DZHotSpotMetaData.ActionData a = new DZHotSpotMetaData.ActionData()
                    {
                        LinkType = ainfo.LinkType,
                        LinkData = ainfo.LinkData,
                        LinkActivatedByKeys = ConvertKeyEnums(ainfo.KeysToActivate),
                        LinkActivatedByLeftClick = ainfo.LeftClickToActivate
                    };
                    Trace.WriteLine(" -> type " + a.LinkType + " - data: " + a.LinkData);
                    result.Add(a);
                });

            return result;
        }

        /// <summary>
        /// Dumb translation routine that will map from the Visual enum space to the
        /// DZHS namespace. Oh the things we do for clean code seperation!
        /// </summary>
        /// <param name="activationKeys"></param>
        /// <returns></returns>
        private static int ConvertKeyEnums(HotSpotActionBase.ActivationKeys activationKeys)
        {
            int result = (int)DZHotSpotMetaData.ActionData.KeyPressed.NoKey;
            if ((activationKeys & HotSpotActionBase.ActivationKeys.KeyDownArrow) != 0)
            {
                result |= (int)DZHotSpotMetaData.ActionData.KeyPressed.ArrowDown;
            }
            if ((activationKeys & HotSpotActionBase.ActivationKeys.KeyUpArrow) != 0)
            {
                result |= (int)DZHotSpotMetaData.ActionData.KeyPressed.ArrowUp;
            }
            if ((activationKeys & HotSpotActionBase.ActivationKeys.KeyLeftArrow) != 0)
            {
                result |= (int)DZHotSpotMetaData.ActionData.KeyPressed.ArrowLeft;
            }
            if ((activationKeys & HotSpotActionBase.ActivationKeys.KeyRightArrow) != 0)
            {
                result |= (int)DZHotSpotMetaData.ActionData.KeyPressed.ArrowRight;
            }
            return result;
        }

        private string SubMeetingDir { get; set; }

        /// <summary>
        /// This visual is a sparse image -- run it, and return the result!
        /// </summary>
        /// <param name="v"></param>
        /// <returns></returns>
        private FileInfo ConvertComposition(Visual root, DirectoryInfo meetingDir)
        {
            ///
            /// If this is a place holder for a previously rendered image, then we can
            /// take a quick shortcut here.
            /// 

            VisualItemPlaceHolder place = root as VisualItemPlaceHolder;
            if (place != null)
            {
                return place.XMLFile;
            }

            //
            // If this guy is zero in size, then we bug out of here.
            //

            if (root.Width == 0 || root.Height == 0)
                return null;

            ///
            /// Rescale it to be the proper size...
            /// 

            double oldWidth = root.Width;
            double scale = 1.0 / oldWidth;
            root.Scale(scale);

            var oldRootBB = root.VisualBoundingBox;
            root.XPosition = 0.0;
            root.YPosition = 0.0;

            ///
            /// Generate the base rendering dir
            /// 

            string baseRenderDir = root.ComposableUnitName;
            if (baseRenderDir == null)
            {
                baseRenderDir = Path.GetFileNameWithoutExtension(Path.GetRandomFileName());
            }

            ///
            /// Create a single sparse image too for this guy
            /// 

            try
            {
                ReportVisualStatus(root, baseRenderDir, "Starting DeepZoom rendering");
                Microsoft.DeepZoomTools.SparseImageCreator si = new Microsoft.DeepZoomTools.SparseImageCreator();
                si.CopyMetadata = false;
                si.ImageQuality = ImageQuality;
                si.TileFormat = Microsoft.DeepZoomTools.ImageFormat.Png;
                si.TileSize = 256;
                si.TileOverlap = 1;
                var imageList = ConvertVisualsToImages(root).ToArray();

                string dirName = meetingDir.FullName + "\\" + SubMeetingDir + "\\" + baseRenderDir;

                ///
                /// No need to re-run if the file already exists and the token file is also there!
                /// 

                FileInfo finalXMLFile = new FileInfo(Path.ChangeExtension(dirName, ".xml"));

                si.Create(imageList, dirName);

                finalXMLFile.Refresh();

                ///
                /// Before doing the rescaling - we need to extract all the hot spot info here!
                /// Make sure we do a deep dive into things to get everything!
                /// 

                ExtractHSInfo(dirName + "-HotSpotdata.xml", root.GetHierarchicalEnumerator(ignore => false));

                ///
                /// Next, save this in the database, if the database exists!
                /// 

                if (MyVisualDB != null && root.IsCompositionUnit)
                {
                    CacheDBInfo info = new CacheDBInfo();
                    info.ComposeInfo = root.CompositionInfo;
                    info.DZXMLFile = finalXMLFile;
                    info.Metadata = root.Info;
                    MyVisualDB.Save(info);
                }

                ///
                /// Get the scalling back
                /// 

                root.Scale(oldWidth);
                root.XPosition = oldRootBB.XPosition;
                root.YPosition = oldRootBB.YPosition;

                ///
                /// Return the file for use by the grand composition
                /// 

                return finalXMLFile;
            }
            catch (Exception e)
            {
                MyErrorLogger.LogMessage("SubMeetingRender", "Error rendering submeeting in directory " + baseRenderDir, e);
                return null;
            }
            finally
            {
                ReportVisualStatus(root, baseRenderDir, "Finished DeepZoom rendering");
            }
        }

        /// <summary>
        /// Report how the rendering went....
        /// </summary>
        /// <param name="root"></param>
        /// <param name="p"></param>
        private void ReportVisualStatus(Visual root, string id, string message)
        {
            StringBuilder bld = new StringBuilder();
            bld.AppendFormat("{0}", id);
            if (root.Info != null && root.Info.Title != null)
            {
                bld.AppendFormat(": {0}", root.Info.Title);
            }
            bld.AppendFormat(" - {0}", message);
            MyErrorLogger.LogMessage("SubMeetingRender", bld.ToString());
        }

        /// <summary>
        /// Returns an iterator that will convert all the visuals to Image objects suitable for
        /// stuffing into a image ceator (or whatnot).
        /// </summary>
        /// <returns></returns>
        /// <remarks>This is where the z ordering of the images is defined within one composable unit. The earlier an image is in the list,
        /// the further to the back it is. So z index of 0 is at the very back, and zorder of 100 is near the front.</remarks>
        IEnumerable<Microsoft.DeepZoomTools.Image> ConvertVisualsToImages(Visual root)
        {
            if (root == null)
            {
                root = new VisualHolder();
            }
            var result = from s in root.SGNodeList
                         orderby s.ZOrder ascending
                         select ConvertSGToImage(s);
            return result;
        }

        /// <summary>
        /// Converts a single sceen graph node to a list.
        /// 
        /// The coordinate system used by images is a bit... errr, funny.
        /// First (0,0) is at the upper left hand corner. Right and down are both negative. So it is like looking
        /// at the lower right hand corner of a traditional x,y coordinate system. Now the x-position is
        /// really how many "widths" away it is from the origin. For example, if it is half the size of
        /// the view port, then a value of -1 means it is starts half way across the screen!! So each
        /// image, in this sense, has a seperate coordinate system. And the y coordinate uses the x as
        /// its "basis" units. Fu-barrred!!!
        /// 
        /// Second you have to tell it how big to make it. A value of 1.0 means that it will fill the entire
        /// view port (0->1). A value of 2 means it will be half, etc. Basically, 1/size. This is stored in
        /// ViewPortWidth.
        /// 
        /// </summary>
        /// <param name="s"></param>
        /// <returns></returns>
        private Microsoft.DeepZoomTools.Image ConvertSGToImage(SceneGraphSceneNode s)
        {
            ///
            /// The coordinate system that is understood by the sparse image tool is not your Dad's coordinate system.
            /// 
            /// The second problem is the size.

            Microsoft.DeepZoomTools.Image result = new Microsoft.DeepZoomTools.Image(s.FileName);

            ///
            /// For the view port width we assume that the max width is 1.0 for everything we are doing here.
            /// And that in the mean time this was done with a global rescaling. So the width of this image
            /// is going to be just 1/ its view port width.
            /// 

            result.ViewportWidth = 1.0 / s.Width;

            ///
            /// Now we have to figure out how many "widths" over and down this guy is positioned
            /// and that will be the position.
            /// 

            result.ViewportOrigin = new System.Windows.Point(
                -(s.x / s.Width), -(s.y / s.Width)
                );

            return result;
        }

        /// <summary>
        /// Count the number of sub images that were rendered for this guy
        /// </summary>
        /// <param name="dz"></param>
        /// <returns></returns>
        internal int NumberOfSubImages(DirectoryInfo dz)
        {
            DirectoryInfo sc = new DirectoryInfo(dz.FullName + "\\sc");
            if (!sc.Exists)
            {
                return 0;
            }

            return sc.GetDirectories().Length;
        }
    }
}
